Displaying Rich Text on Android Using The Contentful SDK

Contentful is a CMS that allows you to easily distribute content to all types of platforms including websites, iOS and android apps. It provides you with an easy-to-use web app where you can manage your content and provide it to all your platforms. The content that you want to provide can be one of many types, each with their own use.

Rich Text

One of the content types is Rich Text. This allows you to format your text to give it more structure, add hyperlinks to external resources or even insert a code block or an image. This can be very useful for content where a lot of text is present, like a blogpost or an article.

The Rich Text editor of the Contentful Web App

If we want to display this rich text in the way that we input it into the web app, we'll need to have a renderer that is capable of handling all the different styles. Today we'll take a look at how the Contentful Android SDK provides this data, and how we can render it nicely.

Generating Some Sample Content

Before we can access our content in the android app, we will have to provide it. We create a new Blogpost content model. Our model will have a Rich Text field called body that we can use to write our blogposts. Then we go over to the content tab and write a little blogpost.

Our little example blogpost int the Contentful Rich Text Editor

Finally we publish the post, and our sample content is done.

Reading the data in Android

To receive our sample content in android we will be using the official Contentful Android SDK.

Getting our content is simple. First, we initiate the Content Delivery API Client. Then we can use said client to retrieve our sample blogpost. We'll be using rxAndroid to help us with making asynchronous network calls.

// Building the CDAClient

val client = CDAClient.builder()
            .setSpace("{space-key-goes-here}")
            .setToken("{access-token-goes-here}")
            .build()

// Retrieving the sample blogpost

client.observe(CDAEntry::class.java)
            .one("{entry-id-goes-here}")
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeOn(Schedulers.io())
            .subscribe({ entry ->
                val body = entry.getField<CDARichDocument>("body")
            }) {
                Log.e("Contentful Error", it.toString())
            }

If we take a look at the entry, it shows that it has our body field that contains a CDARichDocument object. Reading out the body using the getField method shows that it contains a list of objects for all our elements. We can clearly see our blogpost structure with headings, paragraphs, lists, hyperlinks, quotes and images, all in their own object type.

Rendering the Rich Text

Now that we have our data, we'll have to render it. We could write our own mappers to transform the data into renderable views, however that will be very time consuming. Luckily for us, the Contentful team themselves are working on a Rich Text Renderer for android.

Attention: At the time of writing, this library is still in beta. The developers did recently say that they are trying to get the library production ready in the coming weeks.

With this library we create a processor that allows us to process a CDARichDocument node. We can render to android in two different ways: through spannables or through custom views. We'll take a look at both ways.

Spannables

To work with spannables we'll use the sequenceProcessor. This will create a SpannableStringBuilder that we can then use on a TextView.

client.observe(CDAEntry::class.java)
            .one("{entry-id-goes-here}")
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeOn(Schedulers.io())
            .subscribe({entry ->
                val body = entry.getField<CDARichDocument>("body")

                val context = AndroidContext(this)

                val sequenceProcessor = AndroidProcessor.creatingCharSequences()
                val sequenceResult = sequenceProcessor.process(context, body)

                textView.setText(sequenceResult, TextView.BufferType.SPANNABLE)

            }) {
                Log.e("Error", it.toString())
            }

And this is the result:

We can immediately see that the result is not what we desired. The most obvious issues are:

  • Everything is placed on one line, there are no linebreaks at all places that we expect.
  • The image is not rendered at all.
  • While the hyperlink is styled like a link, clicking it does nothing.
  • Quotes aren't rendered as expected.
  • The horizontal ruler is not the full width of the screen.

With all these issues, this method is far from ideal.

Custom Views

The library also has a viewProcessor that generates a LinearLayout full of custom views for all the elements. Let's try out that one:

client.observe(CDAEntry::class.java)
            .one("{entry-id-goes-here}")
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeOn(Schedulers.io())
            .subscribe({entry ->
                val body = entry.getField<CDARichDocument>("body")

                val context = AndroidContext(this)

                val viewProcessor = AndroidProcessor.creatingNativeViews()
                val viewResult = viewProcessor.process(context, body)

                viewResult?.setPadding(30, 30, 30, 30)
                container.addView(viewResult)

            }) {
                Log.e("Error", it.toString())
            }

This time around, the results look way more promising:

There are still a few issues though:

  • The image is not rendered at all.
  • The hyperlink works but is rendered in a very unique way.

The hyperlink style is an interesting one. let's dig through the code a bit to check how the renderer gets to this result.

Analysing the hyperlink code

In the AndroidProcessor code we see that the viewProcessor uses a NativeViewsRendererProvider.

// AndroidProcessor.java

public class AndroidProcessor<T> extends Processor<AndroidContext, T> {

  ...

  public static AndroidProcessor<View> creatingNativeViews() {
    final AndroidProcessor<View> processor = new AndroidProcessor<>();
    new NativeViewsRendererProvider().provide(processor);

    return processor;
  }

  ...
}

Looking in this NativeViewsRendererProvider we see that it calls a number of different renderers, including one for hyperlinks.

// NativeViewsRendererProvider.java

public class NativeViewsRendererProvider {
  
  public void provide(@Nonnull AndroidProcessor<View> processor) {
    processor.addRenderer(new TextRenderer(processor));
    processor.addRenderer(new HorizontalRuleRenderer(processor));
    processor.addRenderer(new ListRenderer(processor, new BulletDecorator()));
    processor.addRenderer(new ListRenderer(processor,
        new NumbersDecorator(),
        new UpperCaseCharacterDecorator(),
        new LowerCaseRomanNumeralsDecorator(),
        new LowerCaseCharacterDecorator(),
        new LowerCaseCharacterDecorator(),
        new UpperCaseRomanNumeralsDecorator()
    ));
    processor.addRenderer(new HyperLinkRenderer(processor));
    processor.addRenderer(new QuoteRenderer(processor));
    processor.addRenderer(new BlockRenderer(processor));
  }
}

Looking at the HyperLinkRenderer we can see that it adds an onClick listener to handle the actual linking, and we also see that it inflates R.layout.rich_hyperlink_layout.

<!--rich_hyperlink_layout.xml-->

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="#40808080"
    >

    <ImageView
        android:id="@+id/rich_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:contentDescription="@null"
        android:padding="4dp"
        android:src="@android:drawable/ic_menu_share"
        />

    <LinearLayout
        android:id="@id/rich_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_toEndOf="@id/rich_image"
        android:gravity="center"
        android:orientation="vertical"
        />
</RelativeLayout>

We can clearly see that this layout matches the hyperlink style in the app. If you don't like the way this or any other elements are styled, don't worry! The library has a way to add or override a style and change it to your liking.

Adding or overriding renderers

The processor has two methods to alter the renderers: .addRenderer(…,…) and .overrideRenderer(…,…). Both these methods have two parameters that are each lambda functions.

The first lambda is called the checker. This function gives you the node and expects a boolean in return. It allows you to check if this renderer handles this node (e.g. is this node a hyperlink? or is this node an image?)

The second lambda is the actual renderer. It gives you the node and expects a view in return. This one will only be called if the checker returns true.

The difference between addRenderer and overrideRenderer is when they are called. addRenderer will be added to the end of the list, whereas overrideRenderer will be added to the start. Each node will only trigger the first checker that matches.

Adding a custom image renderer

Let's try out these functions by writing a custom renderer to render the images. As we saw in the body node, an image is a CDARichEmbeddedBlock containing a CDAAsset. So we need to check for these types in our checker. To fetch and show the actual image we will use Picasso.

viewProcessor.overrideRenderer(
                    // Checker
                    { _, node -> node is CDARichEmbeddedBlock && node.data is CDAAsset },

                    // Renderer
                    { _, node ->
                        val data = (node as CDARichEmbeddedBlock).data as CDAAsset

                        val imageview = ImageView(this).apply {
                            layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
                        }

                        Picasso.get().load("https:" + data.url()).into(imageview)

                        imageview
                    }
                )
Please note that we use overrideRenderer and not addRenderer. This is because our image node will trigger the checker of the default BlockRenderer. As a result, our renderer would never be called if we were to use addRenderer.

And if we look in our app we see that now it will also render our image:

And that's it!

We can now render contentful rich text fields in android and know how to add or override elements if we want to style them differently.